#.ExternalHelp ADAudit.Help.xml
Function Convert-GUIDtoLDAPGUID{
	[CmdletBinding(DefaultParameterSetName='Named')]
	Param(
		[Parameter(
			Mandatory=$true,
			Position=0,
			ParameterSetName='Input',
			ValueFromPipeline=$true
		)]
		$InputObject,
	
		[Parameter(
			Mandatory=$true,
			Position=0,
			ParameterSetName='Named'
			)]
		[String]$GUID
	)
	
	Process{
		if ($psCmdlet.ParameterSetName -eq 'Input'){
			Write-Verbose "[Convert-GUIDToLDAPGUID] - `tValue from Pipeline is TRUE"
			Write-Verbose "[Convert-GUIDToLDAPGUID] - `tInput Object Type is '$($InputObject.GetType().FullName)'"
			
			#Is supported type?
			#==================
			switch ($InputObject.GetType().FullName){
			
				'Microsoft.ActiveDirectory.Management.ADUser'{
					$strGUID = $InputObject.ObjectGUID.Guid
				}
				
				'System.String'{
					$strGUID = $InputObject
				}
				
				'System.Guid'{
					$strGUID = $InputObject.Guid
				}
				
				default{
					Write-Error 'Input Object class is non-parseable'
					break
				}
			}
		}
		else{
			Write-Verbose "[Convert-GUIDToLDAPGUID] - `tValue from Pipeline is FALSE"
			Write-Verbose "[Convert-GUIDToLDAPGUID] - `tInput Object Type is '$($GUID.GetType().FullName)'"
			
			#Assume Named input
			#==================
			$strGUID = $GUID
		}
		
		Write-Verbose "[Convert-GUIDToLDAPGUID] - `tGUID String Value is '$strGUID'"
		
		return ((([Guid]$strGUID).ToByteArray() | % { '\' + $_.ToString('x2')}) -join '')
		
	}
}

#.ExternalHelp ADAudit.Help.xml
Function Get-ADGroupMembershipAudit{
	[CmdletBinding(DefaultParameterSetName="Named")]
	Param(
		[Parameter(
			Mandatory=$true,
			Position=0,
			ValueFromPipeline=$true,
			ParameterSetName='Input'
		)]
		$InputObject,
		
		[Parameter(
			Mandatory=$true,
			Position=0,
			ParameterSetName='Named'
		)][string[]]$Identity
	)

	Begin {
		$rootPartition = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext
		Write-Verbose "[Get-ADGroupMembershipAudit] - `tDefault naming context is '$rootPartition'"
	}
	
	Process {
		if ($psCmdlet.ParameterSetName -eq 'Input'){		
			Write-Verbose "[Get-ADGroupMembershipAudit] - `tValue from Pipeline is TRUE"
			Write-Verbose "[Get-ADGroupMembershipAudit] - `tInput Object Type is '$($InputObject.GetType().FullName)'"
		
			#Is supported type?
			#==================
			switch ($InputObject.GetType().FullName){
			
				'Microsoft.ActiveDirectory.Management.ADGroup' {
					$searchRoot = $InputObject.DistinguishedName
					$filter = "(objectCategory=*)"
				}
				
				'System.String' {
					$searchRoot = $rootPartition
					$filter = "(|(&(name=$InputObject)(objectCategory=group))(&(cn=$InputObject)(objectCategory=group))(&(distinguishedname=$InputObject)(objectCategory=group)))"
				}
				
				default {
					Write-Error "Input Object class is non-parseable"
					break
				}
			}
		}
		else {
			Write-Verbose "[Get-ADGroupMembershipAudit] - `tValue from Pipeline is FALSE"
			Write-Verbose "[Get-ADGroupMembershipAudit] - `tInput Object Type is '$($Identity.GetType().FullName)'"
			
			#Assume Named input
			#==================
			$searchRoot = $rootPartition
			$filter = "(|(&(name=$Identity)(objectCategory=group))(&(cn=$Identity)(objectCategory=group))(&(distinguishedname=$Identity)(objectCategory=group)))"
		}
		
		#Construct Search Object to find Group
		#=====================================
		$srchGroup = [DirectoryServices.DirectorySearcher]"$filter"
		$srchGroup.SearchRoot = "LDAP://$searchRoot"
		$srchGroup.PropertiesToLoad.Add("distinguishedname") | Out-Null
		$srchGroup.PropertiesToLoad.Add("cn") | Out-Null
		
		Write-Verbose "[Get-ADGroupMembershipAudit] - `tRetrieving Group Object from Directory"
		Write-Verbose "[Get-ADGroupMembershipAudit] - `tSearch Root is '$searchRoot'"
		Write-Verbose "[Get-ADGroupMembershipAudit] - `tLDAP Filter is '$filter'"
		
		$result = $srchGroup.FindOne() #Execute Search
	
		if ($result -eq $null){
			Write-Warning "Group Object not found"
			break
		}
		
		$dn = $result.properties.distinguishedname[0]
		$cn = $result.properties.cn[0]
		
		#Retrieve raw replication metadata
		#=================================
		$meta = (Get-ADObjectPagedAttribute $dn "msds-replvaluemetadata")
		$arrResults = @()
		
		#Convert into Object
		#===================
		if ($meta -eq $null){
			Write-Warning "No matching replication data found"
			break
		} 
		else {
			Write-Verbose "[Get-ADGroupMembershipAudit] - `tConverting XML output into PSObject"
			foreach ($line in $meta){
				$thisUser = [XML]$line
				$obj = New-Object -TypeName PSObject | Select-Object "Group", "Member", "Operation", "Timestamp"
				$obj.Group = $cn
				$obj.Member = $thisUser."DS_REPL_VALUE_META_DATA".pszObjectDn
				
				$timeAdded = [datetime]$thisUser."DS_REPL_VALUE_META_DATA".fTimeCreated
				$timeRemoved = [datetime]$thisUser."DS_REPL_VALUE_META_DATA".fTimeDeleted
				$timeChanged = [datetime]$thisUser."DS_REPL_VALUE_META_DATA".ftimeLastOriginatingChange
				
				$obj.Timestamp = $timeChanged
				
				if ($timeRemoved -eq '01/01/1601'){
					$obj.Operation = "Added"
				}
				else {
					$obj.Operation = "Removed"
				}
				$arrResults+=$obj
			}
		}
		
		Write-Verbose "[Get-ADGroupMembershipAudit] - `tReturned $($arrResults.Count) Objects"
		return $arrResults
	}
}

#.ExternalHelp ADAudit.Help.xml
Function Get-ADObjectPagedAttribute{
	[CmdletBinding(DefaultParameterSetName="Named")]
	Param(
		[Parameter(
			Mandatory=$true,
			Position=0,
			ValueFromPipeline=$true,
			ParameterSetName='Input'
		)]
		$InputObject,
		
		[Parameter(
			Mandatory=$true,
			Position=0,
			ParameterSetName='Named'
		)][string[]]$Identity,
		
		[Parameter(
			Mandatory=$true,
			Position=1
		)][string]$Attribute
	)
	
	Begin {
		Function Get-DN {
			Param(
				[string]$searchRoot,
				[string]$searchString
			)
			
			#Construct Search
			#================
			$searchRoot = $rootPartition
			$filter = "(|(name=$searchString)(userPrincipalName=$searchString)(cn=$searchString)(samaccountname=$searchString)(distinguishedname=$searchString))"
			
			$srchUser = [DirectoryServices.DirectorySearcher]"$filter"
			$srchUser.SearchRoot = "LDAP://$searchRoot"
			$srchUser.PropertiesToLoad.Add("distinguishedname") | Out-Null
			
			$result = $srchUser.FindOne() #Execute Search

			if ($result -eq $null){
				return $null
			} else {
				return $result.Properties.distinguishedname[0]
			}
		}
		
		#Get RootDSE for current domain
		#==============================
		$rootPartition = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext
		Write-Verbose "[Get-ADObjectPagedAttribute] - `tDefault naming context is '$rootPartition'"
	}
	
	Process {
		if ($psCmdlet.ParameterSetName -eq 'Input'){
			Write-Verbose "[Get-ADObjectPagedAttribute] - `tValue from Pipeline is TRUE"
			Write-Verbose "[Get-ADObjectPagedAttribute] - `tInput Object Type is '$($InputObject.GetType().FullName)'"
			
			#Is supported type?
			#==================
			switch ($InputObject.GetType().FullName){
			
				'Microsoft.ActiveDirectory.Management.ADUser' {
					$userDN = $InputObject.DistinguishedName
				}
				
				'Microsoft.ActiveDirectory.Management.ADGroup' {
					$userDN = $InputObject.DistinguishedName
				}
				
				'System.String' {
					$userDN = Get-DN -searchRoot $rootPartition -searchString $InputObject
					
					if ($userDN -eq $null){
						Write-Warning "User Object not found"
						break
					}
				}
				
				default {
					Write-Error "Input Object class is non-parseable"
					break
				}
			}
		}
		else {
			Write-Verbose "[Get-ADObjectPagedAttribute] - `tValue from Pipeline is FALSE"
			Write-Verbose "[Get-ADObjectPagedAttribute] - `tInput Object Type is '$($Identity.GetType().FullName)'"
			
			$userDN = Get-DN -searchRoot $rootPartition -searchString $Identity
			
			if ($userDN -eq $null){
				Write-Warning "User Object not found"
				break
			}
		}
	
		#Construct Searcher Object
		#=========================
		Write-Verbose "[Get-ADObjectPagedAttribute] - `tRetrieve '$Attribute' attribute for DN '$userDN'"
		$adObject = New-Object System.DirectoryServices.DirectoryEntry -ArgumentList @("LDAP://$userDN")
		$srch = New-Object System.DirectoryServices.DirectorySearcher -ArgumentList @($adObject)
		$srch.Filter = "(objectCategory=*)"

		#Counters
		#========
		[Int32]$rangeStep = 1000
		Write-Verbose "[Get-ADObjectPagedAttribute] - `tAttribute step increment: $rangeStep"
		[Int32]$rangeLow = 0
		[Int32]$rangeHigh = $rangeLow + ($rangeStep - 1)
		[bool]$lastQuery = $false
		[bool]$quitLoop = $false

		$arrResults = @()

		#Retrieve paged property
		#=======================
		do {
			if (!$lastQuery){
				$attributeWithRange = "$Attribute;range={0}-{1}" -f @($rangeLow, $rangeHigh)
			}
			
			#Search for specific ranged attribute
			#i.e 'member;range=1000-1999'
			#============================
			$srch.PropertiesToLoad.Clear()
			$srch.PropertiesToLoad.Add($attributeWithRange) | Out-Null
			Write-Verbose "[Get-ADObjectPagedAttribute] - `tRetrieving $attributeWithRange"
			$result = $srch.FindOne()
			
			#If this is the last property page then
			#attribute will look like 'member;range=1000-*'
			#So set flag value to quit loop
			#==============================
			if ($result.Properties.Contains($attributeWithRange)){
				$arrResults+=$result.Properties[$attributeWithRange]
				if ($lastQuery){
					$quitLoop = $true
				}
			}
			else {
				$lastQuery = $true
			}
			
			#Increment paged property counter
			#or quit loop
			#============
			if (!$lastQuery){
				$rangeLow = $rangeHigh+1
				$rangeHigh = $rangeLow + ($rangeStep - 1)
			} 
			else {
				$attributeWithRange = "$Attribute;range={0}-*" -f $rangeLow
				Write-Verbose "[Get-ADObjectPagedAttribute] - `tEnd of range, retrieving $attributeWithRange"
				$arrResults+=$result.Properties[$attributeWithRange]
				$quitLoop = $true
			}
			
		} while (!$quitLoop)

		Write-Verbose "[Get-ADObjectPagedAttribute] - `tTotal Property Count $($arrResults.count)"

		return $arrResults
	}
}

#.ExternalHelp ADAudit.Help.xml
Function Get-ADUserAttributeChanges {
	[CmdletBinding(DefaultParameterSetName="Named")]
	Param(
		[Parameter(
			Mandatory=$true,
			Position=0,
			ValueFromPipeline=$true,
			ParameterSetName='Input'
		)]
		$InputObject,
		
		[Parameter(
			Mandatory=$true,
			Position=0,
			ParameterSetName='Named'
		)][string[]]$Identity,
		
		[Parameter(
			Position=1
		)]
		[String[]]$Attribute="All"
	)
	
	Begin {	
		#Get DC object (Required to get Replication MetaData)
		#====================================================
		$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name
		$context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$domain)
		$dc = [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($context)
		$rootPartition = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext
		
		Write-Verbose "[Get-ADUserAttributeChanges] - `tCurrent Domain: $domain"
		Write-Verbose "[Get-ADUserAttributeChanges] - `tConnecting to Domain Controller '$dc'"
		Write-Verbose "[Get-ADUserAttributeChanges] - `tDefault naming context is '$rootPartition'"
	}
	
	Process {
		if ($psCmdlet.ParameterSetName -eq 'Input'){
			Write-Verbose "[Get-ADUserAttributeChanges] - `tValue from Pipeline is TRUE"
			Write-Verbose "[Get-ADUserAttributeChanges] - `tInput Object Type is '$($InputObject.GetType().FullName)'"
			
			#Is supported type?
			#==================
			switch ($InputObject.GetType().FullName){
			
				'Microsoft.ActiveDirectory.Management.ADUser' {
					$searchRoot = $InputObject.DistinguishedName
					$filter = "(objectCategory=*)"
				}
				
				'System.String' {
					$searchRoot = $rootPartition
					$filter = "(|(&(userPrincipalName=$InputObject)(objectCategory=person))(&(name=$InputObject)(objectCategory=person))(&(cn=$InputObject)(objectCategory=person))(&(samaccountname=$InputObject)(objectCategory=person))(&(distinguishedname=$InputObject)(objectCategory=person)))"
				}
				
				default {
					Write-Error "Input Object class is non-parseable"
					break
				}
			}
		}
		else {
			Write-Verbose "[Get-ADUserAttributeChanges] - `tValue from Pipeline is FALSE"
			Write-Verbose "[Get-ADUserAttributeChanges] - `tInput Object Type is '$($Identity.GetType().FullName)'"
			
			#Assume Named input
			#==================
			$searchRoot = $rootPartition
			$filter = "(|(&(userPrincipalname=$Identity)(objectCategory=person))(&(name=$Identity)(objectCategory=person))(&(cn=$Identity)(objectCategory=person))(&(samaccountname=$Identity)(objectCategory=person))(&(distinguishedname=$Identity)(objectCategory=person)))"
		}
		
		#Construct Search Object to find User
		#====================================
		$srchUser = [DirectoryServices.DirectorySearcher]"$filter"
		$srchUser.SearchRoot = "LDAP://$searchRoot"
		$srchUser.PropertiesToLoad.Add("distinguishedname") | Out-Null
		$srchUser.PropertiesToLoad.Add("cn") | Out-Null
		
		Write-Verbose "[Get-ADUserAttributeChanges] - `tRetrieving User Object from Directory"
		Write-Verbose "[Get-ADUserAttributeChanges] - `tSearch Root is '$searchRoot'"
		Write-Verbose "[Get-ADUserAttributeChanges] - `tLDAP Filter is '$filter'"
		
		$result = $srchUser.FindOne() #Execute Search
		
		if ($result -eq $null){
			Write-Warning "User Object not found"
			break
		}
		
		$dn = $result.properties.distinguishedname[0]
		
		#Get Attribute Changes for User
		#==============================
		$meta = $dc.GetReplicationMetadata($dn)
		$arrVal = @()
		
		foreach ($attr in $meta.AttributeNames){
			if ($meta["$attr"] -ne $null){
				$arrVal+=$meta["$attr"]
			}
		}
		#$meta.attributenames | % {$arrVal+=$meta.$_}
		$arrVal | Add-Member -MemberType AliasProperty -Name Timestamp -Value LastOriginatingChangeTime
		$arrVal | Add-Member -MemberType AliasProperty -Name Attribute -Value Name
		$arrVal | Add-Member -MemberType NoteProperty -Name User -Value $result.properties.cn[0]
		
		if ($Attribute -contains "All"){
			Write-Verbose "[Get-ADUserAttributeChanges] - `tReturning ALL attributes"
			return $arrVal
		} 
		else {
			Write-Verbose "[Get-ADUserAttributeChanges] - `tReturning attribute(s) '$Attribute' only"
			return ($arrVal | ? {$Attribute -contains $_.Name})
		}
	}
}

#.ExternalHelp ADAudit.Help.xml
Function Get-ADUserLogonTimes{
	[CmdletBinding(DefaultParameterSetName="Named")]
	Param(
		[Parameter(
			Mandatory=$true,
			Position=0,
			ValueFromPipeline=$true,
			ParameterSetName='Input'
		)]
		$InputObject,
		
		[Parameter(
			Mandatory=$true,
			Position=0,
			ParameterSetName='Named'
		)][string[]]$Identity
	)
	
	Begin {
		$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name
		$context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$domain)
		$dcs = [System.DirectoryServices.ActiveDirectory.DomainController]::FindAll($context) | Select -ExpandProperty Name
		$rootPartition = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext
		
		Write-Verbose "[Get-ADUserLogonTimes] - `tCurrent Domain: $domain"
		Write-Verbose "[Get-ADUserLogonTimes] - `tRetrieved Domain Controllers '$dcs'"
		Write-Verbose "[Get-ADUserLogonTimes] - `tDefault naming context is '$rootPartition'"
	}
	
	Process {
		if ($PSCmdlet.ParameterSetName -eq 'Input'){
			Write-Verbose "[Get-ADUserLogonTimes] - `tValue from Pipeline is TRUE"
			Write-Verbose "[Get-ADUserLogonTimes] - `tInput Object Type is '$($InputObject.GetType().FullName)'"
			
			#Is supported type?
			#==================
			switch ($InputObject.GetType().FullName){
			
				'Microsoft.ActiveDirectory.Management.ADUser'{
					$searchRoot = $InputObject.DistinguishedName
					$filter = "(objectCategory=*)"
				}
				
				'System.String' {
					$searchRoot = $rootPartition
					$filter = "(|(&(userprincipalname=$InputObject)(objectCategory=person))(&(name=$InputObject)(objectCategory=person))(&(cn=$InputObject)(objectCategory=person))(&(samaccountname=$InputObject)(objectCategory=person))(&(distinguishedname=$InputObject)(objectCategory=person)))"
				}
				
				default {
					Write-Error "No handler for class $($InputObject.GetType().FullName)"
					$exit = $true
					break
				}
			}
		}
		else {
			Write-Verbose "[Get-ADUserLogonTimes] - `tValue from Pipeline is FALSE"
			Write-Verbose "[Get-ADUserLogonTimes] - `tInput Object Type is '$($Identity.GetType().FullName)'"
			
			#Assume Named input
			#==================
			$searchRoot = $rootPartition
			$filter = "(|(&(userPrincipalName=$Identity)(objectCategory=person))(&(name=$Identity)(objectCategory=person))(&(cn=$Identity)(objectCategory=person))(&(samaccountname=$Identity)(objectCategory=person))(&(distinguishedname=$Identity)(objectCategory=person)))"
		}
		
		#Quit if unhandled type
		#======================
		if ($exit){break}
	
		$arrLogons = @()
		
		$srchUser = [DirectoryServices.DirectorySearcher]"$filter"
		$srchUser.SearchRoot = "LDAP://$searchRoot"
		$srchUser.PropertiesToLoad.Add("distinguishedname") | Out-Null
		$srchUser.PropertiesToLoad.Add("cn") | Out-Null
		
		Write-Verbose "[Get-ADUserLogonTimes] - `tRetrieving User Object from Directory"
		Write-Verbose "[Get-ADUserLogonTimes] - `tSearch Root is '$searchRoot'"
		Write-Verbose "[Get-ADUserLogonTimes] - `tLDAP Filter is '$filter'"
		
		$result = $srchUser.FindOne()
		
		if ($result -eq $null){
			Write-Warning "Object '$Identity' not found"
			break
		}
		
		#Get Directory Object
		#====================
		$user = $result.GetDirectoryEntry()
		$dn = $user.properties.distinguishedname[0]
		$cn = $user.Properties.cn[0]
		
		#Get replicated value
		#====================
		$x = New-Object PSObject | Select-Object "Source", "LogonTime", "Username"
		$x.Username = $cn
		$x.Source = "Replicated"
		Write-Verbose "[Get-ADUserLogonTimes] - `tRetrieving Replicated 'lastlogontimestamp'"
		if ($user.Properties.Contains('lastlogontimestamp')){
			$x.LogonTime = [system.datetime]::FromFileTime($user.ConvertLargeIntegerToInt64($user.properties.lastlogontimestamp[0]))
		} 
		else {
			$x.LogonTime = $null
		}
		$arrLogons+=$x
		
		#Get DC Individual Logons
		#========================
		foreach ($DC in $DCs){
			Write-Verbose "[Get-ADUserLogonTimes] - `tRetrieving user logon time from Domain Controller: $DC"
			$user = [ADSI]"LDAP://$DC/$DN"
			$x = New-Object PSObject | Select-Object "Source", "LogonTime", "Username"
			$x.Username = $cn
			$x.Source = $DC
			if ($user.Properties.Contains('lastlogon')){
				$x.LogonTime = [system.datetime]::FromFileTime($user.ConvertLargeIntegerToInt64($user.lastLogon[0]))
			} 
			else {
				$x.LogonTime = $null
			}
			$arrLogons+=$x
		}
		
		return ($arrLogons | Sort-Object Source)
	}
}

#.ExternalHelp ADAudit.Help.xml
Function Get-ADUserMembershipAudit {
	[CmdletBinding(DefaultParameterSetName="Named")]
	Param(
		[Parameter(
			Mandatory=$true,
			Position=0,
			ValueFromPipeline=$true,
			ParameterSetName='Input'
		)]
		$InputObject,
		
		[Parameter(
			Mandatory=$true,
			Position=0,
			ParameterSetName='Named'
		)][string[]]$Identity
	)
	
	Begin {
		#Get DC object (Required to get Replication MetaData)
		#====================================================
		$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name
		$context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext("Domain",$domain)
		$dc = [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($context)
		$rootPartition = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext
		
		Write-Verbose "[Get-ADUserMembershipAudit] - `tCurrent Domain: $domain"
		Write-Verbose "[Get-ADUserMembershipAudit] - `tConnecting to Domain Controller '$dc'"
		Write-Verbose "[Get-ADUserMembershipAudit] - `tDefault naming context is '$rootPartition'"
	}
	
	Process {
		if ($psCmdlet.ParameterSetName -eq 'Input'){
			Write-Verbose "[Get-ADUserMembershipAudit] - `tValue from Pipeline is TRUE"
			Write-Verbose "[Get-ADUserMembershipAudit] - `tInput Object Type is '$($InputObject.GetType().FullName)'"
		
			#Is supported type?
			#==================
			switch ($InputObject.GetType().FullName){
			
				'Microsoft.ActiveDirectory.Management.ADUser' {
					$searchRoot = $InputObject.DistinguishedName
					$filter = "(objectCategory=*)"
				}
				
				'System.String' {
					$searchRoot = $rootPartition
					$filter = "(|(&(userPrincipalName=$InputObject)(objectCategory=person))(&(name=$InputObject)(objectCategory=person))(&(cn=$InputObject)(objectCategory=person))(&(samaccountname=$InputObject)(objectCategory=person))(&(distinguishedname=$InputObject)(objectCategory=person)))"
				}
				
				default {
					Write-Error "Input Object class is non-parseable"
					break
				}
			}
		}
		else {
			Write-Verbose "[Get-ADUserMembershipAudit] - `tValue from Pipeline is FALSE"
			Write-Verbose "[Get-ADUserMembershipAudit] - `tInput Object Type is '$($Identity.GetType().FullName)'"
			
			#Assume Named input
			#==================
			$searchRoot = $rootPartition
			$filter = "(|(&(userPrincipalName=$Identity)(objectCategory=person))(&(name=$Identity)(objectCategory=person))(&(cn=$Identity)(objectCategory=person))(&(samaccountname=$Identity)(objectCategory=person))(&(distinguishedname=$Identity)(objectCategory=person)))"
		}
		
		
		#Construct Search Object to find User
		#====================================
		$srchUser = [DirectoryServices.DirectorySearcher]"$filter"
		$srchUser.SearchRoot = "LDAP://$searchRoot"
		$srchUser.PropertiesToLoad.Add("distinguishedname") | Out-Null
		$srchUser.PropertiesToLoad.Add("cn") | Out-Null
		$srchUser.PropertiesToLoad.Add("memberof") | Out-Null
		
		Write-Verbose "[Get-ADUserMembershipAudit] - `tRetrieving User Object from Directory"
		Write-Verbose "[Get-ADUserMembershipAudit] - `tSearch Root is '$searchRoot'"
		Write-Verbose "[Get-ADUserMembershipAudit] - `tLDAP Filter is '$filter'"
		
		$result = $srchUser.FindOne() #Execute Search
		
		if ($result -eq $null){
			Write-Warning "User Object not found"
			break
		}
		
		$dn = $result.properties.distinguishedname[0]
		$cn = $result.properties.cn[0]
		
		if ($result.properties.Contains("memberof") -eq $false){
			Write-Warning "User '$cn' is not a member in any groups"
			break
		}
		
		$dn = $result.properties.distinguishedname[0]
	
		$arrGroups=@()
		
		#Get Membership Changes for User
		#===============================
		foreach ($group in $result.properties.memberof){			
			Write-Verbose "[Get-ADUserMembershipAudit] - `tRetrieving metadata for membership in '$group'"
			
			$meta = @((Get-ADObjectPagedAttribute $group "msds-replvaluemetadata") | ? {$_ -like "*$dn*"})
		
			foreach ($entry in $meta){
				$obj = New-Object -TypeName PSObject | Select-Object "User", "Group", "DateAdded"
				$obj.User = $cn
				$obj.Group = $group
				$obj.DateAdded = [datetime]([XML]$entry)."DS_REPL_VALUE_META_DATA".ftimecreated
				
				$arrGroups+=$obj
				Remove-Variable entry
			}
		}
		
		return $arrGroups
	}
}

#.ExternalHelp ADAudit.Help.xml
Function New-RandomPassword{
	param(
		[Parameter(
			Mandatory=$true,
			Position=0
		)]
		[int]$Length=8
	)
	
	$password = ""
	$arrChars = @(33..47+58..64+123..126),@(48..57),@(97..122),@(65..90)
	
	#Construct Password
	#==================
	1..($Length-4) | % {$password+="$([char]($arrChars[(Get-Random (0..3))] | Get-Random))"}
	0..3 | % {$password = $password.Insert((Get-Random (0..($password.Length-1))),"$([char]($arrChars[$_] | Get-Random))")}

    return $password
}

#.ExternalHelp ADAudit.Help.xml
Function Test-ADUserCredentials {
	[CmdletBinding()]
	Param(
		[Parameter(
			Mandatory=$true,
			Position=0
		)]
		[string]$Username, 
		
		[Parameter(
			Mandatory=$true,
			Position=1
		)]
		[string]$Password, 
		
		[Parameter(
			Position=2
		)]
		[string]$Domain=([System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain().Name)
	)
	
	#Create credential object and test
	#auth against Domain Controller
	#==============================
	Write-Verbose "[Test-ADUserCredentials] - `tTesting Login for '$Username':'$Password'"
	Add-Type -AssemblyName System.DirectoryServices.AccountManagement
	$ct = [System.DirectoryServices.AccountManagement.ContextType]::Domain
	$pc = New-Object System.DirectoryServices.AccountManagement.PrincipalContext($ct, $domain)
	
	New-Object PSObject -Property @{
		UserName = $username;
		IsValid = $pc.ValidateCredentials($username, $password).ToString()
	}
}

#.ExternalHelp ADAudit.Help.xml
Function Test-ADUserLogonStatus{
	[CmdletBinding(DefaultParameterSetName="Named")]
	Param(
		[Parameter(
			Mandatory=$true,
			Position=0,
			ValueFromPipeline=$true,
			ParameterSetName='Input'
		)]
		$InputObject,
		
		[Parameter(
			Mandatory=$true,
			Position=0,
			ParameterSetName='Named'
		)][string[]]$Identity
	)
	
	Begin {	
		#Get RootDSE for current domain
		#==============================
		$rootPartition = ([ADSI]"LDAP://RootDSE").rootDomainNamingContext
		Write-Verbose "[Test-ADUserLogonStatus] - `tDefault naming context is '$rootPartition'"
	}
	
	Process {
		if ($psCmdlet.ParameterSetName -eq 'Input'){
			Write-Verbose "[Test-ADUserLogonStatus] - `tValue from Pipeline is TRUE"
			Write-Verbose "[Test-ADUserLogonStatus] - `tInput Object Type is '$($InputObject.GetType().FullName)'"
			
			#Is supported type?
			#==================
			switch ($InputObject.GetType().FullName){
			
				'Microsoft.ActiveDirectory.Management.ADUser' {
					$searchRoot = $InputObject.DistinguishedName
					$filter = "(objectCategory=*)"
				}
				
				'System.String' {
					$searchRoot = $rootPartition
					$filter = "(|(&(userPrincipalName=$InputObject)(objectCategory=person))(&(name=$InputObject)(objectCategory=person))(&(cn=$InputObject)(objectCategory=person))(&(samaccountname=$InputObject)(objectCategory=person))(&(distinguishedname=$InputObject)(objectCategory=person)))"
				}
				
				default {
					Write-Error "Input Object class is non-parseable"
					break
				}
			}
		}
		else {
			Write-Verbose "[Test-ADUserLogonStatus] - `tValue from Pipeline is FALSE"
			Write-Verbose "[Test-ADUserLogonStatus] - `tInput Object Type is '$($Identity.GetType().FullName)'"
			
			#Assume Named input
			#==================
			$searchRoot = $rootPartition
			$filter = "(|(&(userPrincipalName=$Identity)(objectCategory=person))(&(name=$Identity)(objectCategory=person))(&(cn=$Identity)(objectCategory=person))(&(samaccountname=$Identity)(objectCategory=person))(&(distinguishedname=$Identity)(objectCategory=person)))"
		}
		
		#Construct Search Object to find User
		#====================================
		$srchUser = [DirectoryServices.DirectorySearcher]"$filter"
		$srchUser.SearchRoot = "LDAP://$searchRoot"
		
		#Properties to retrieve
		#======================
		$srchUser.PropertiesToLoad.Add("distinguishedname") | Out-Null
		$srchUser.PropertiesToLoad.Add("cn") | Out-Null
		$srchUser.PropertiesToLoad.Add("memberof") | Out-Null
		$srchUser.PropertiesToLoad.Add("accountExpires") | Out-Null
		$srchUser.PropertiesToLoad.Add("userAccountControl") | Out-Null
		$srchUser.PropertiesToLoad.Add("msDS-User-Account-Control-Computed") | Out-Null
		$srchUser.PropertiesToLoad.Add("msDS-UserPasswordExpiryTimeComputed") | Out-Null
		$srchUser.PropertiesToLoad.Add("userWorkstations") | Out-Null
		
		Write-Verbose "[Test-ADUserLogonStatus] - `tRetrieving User Object from Directory"
		Write-Verbose "[Test-ADUserLogonStatus] - `tSearch Root is '$searchRoot'"
		Write-Verbose "[Test-ADUserLogonStatus] - `tLDAP Filter is '$filter'"
		
		$result = $srchUser.FindOne() #Execute Search
		
		if ($result -eq $null){
			Write-Warning "User Object not found"
			break
		}
		
		#Construct Object to be returned
		#===============================
		$Object = New-Object -TypeName PSObject | Select Name, DistinguishedName, AccountExpiryDate, IsAccountDisabled, IsAccountLockedOut, IsPasswordExpired, CanLogonToWorkstations, PasswordExpiryDate, UserAccountControl

		#TypeName used for formatting purposes
		#=====================================
		$Object.psobject.TypeNames.Add("ADAudit.ActiveDirectory.User.LogonStatus")
		$Object.Name = $result.Properties.cn[0]
		$Object.DistinguishedName = $result.Properties.distinguishedname[0]
		
		#Test account expiration
		#=======================
		try {
			$Object.AccountExpiryDate = [System.DateTime]::FromFileTime($result.Properties.accountexpires[0])
			if ($Object.AccountExpiryDate -eq '01/01/1601 00:00:00'){
				$Object.AccountExpiryDate = 'Never'
			}
		} 
		catch {
			$Object.AccountExpiryDate = 'Never'
		}
		
		#Test if account is disabled
		#===========================
		if (([int]($result.Properties.useraccountcontrol[0]) -band 2) -eq 2){
			$Object.IsAccountDisabled = $true
		} 
		else {
			$Object.IsAccountDisabled = $false
		}
		
		#Test if account locked out 
		#(useraccountcontrol no longer holds this data)
		#==============================================
		if (([int]($result.Properties.'msds-user-account-control-computed'[0]) -band 0x0010) -eq 0){
			$Object.IsAccountLockedOut = $false
		} 
		else {
			$Object.IsAccountLockedOut = $true
		}
		
		#Test if password expired 
		#(useraccountcontrol no longer holds this data)
		#==============================================
		if (([int]($result.Properties.'msds-user-account-control-computed'[0]) -band 0x800000) -eq 0){
			$Object.IsPasswordExpired = $false
		} 
		else {
			$Object.IsPasswordExpired = $true
		}
		
		#Which workstations can account logon to?
		#========================================
		try{
			$Object.CanLogonToWorkstations = $result.Properties.userworkstations[0]
		}
		catch{
			$Object.CanLogonToWorkstations = 'All'
		}
		
		#When does password expire?
		#==========================
		try{
			$Object.PasswordExpiryDate = [System.DateTime]::FromFileTime($result.Properties.'msds-userpasswordexpirytimecomputed'[0])
		}
		catch{
			$Object.PasswordExpiryDate = 'Never'
		}
		
		#Parse user account control
		#==========================
		$uac = $result.Properties.useraccountcontrol[0]
		$arrUAC = @()
		if ($uac -band 0x00000001){$arrUAC += "ADS_UF_SCRIPT"}
		if ($uac -band 0x00000002){$arrUAC += "ADS_UF_ACCOUNTDISABLE"}
		if ($uac -band 0x00000008){$arrUAC += "ADS_UF_HOMEDIR_REQUIRED"}
		if ($uac -band 0x00000010){$arrUAC += "ADS_UF_LOCKOUT"}
		if ($uac -band 0x00000020){$arrUAC += "ADS_UF_PASSWD_NOTREQD"}
		if ($uac -band 0x00000040){$arrUAC += "ADS_UF_PASSWD_CANT_CHANGE"}
		if ($uac -band 0x00000080){$arrUAC += "ADS_UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED"}
		if ($uac -band 0x00000100){$arrUAC += "ADS_UF_TEMP_DUPLICATE_ACCOUNT"}
		if ($uac -band 0x00000200){$arrUAC += "ADS_UF_NORMAL_ACCOUNT"}
		if ($uac -band 0x00000800){$arrUAC += "ADS_UF_INTERDOMAIN_TRUST_ACCOUNT"}
		if ($uac -band 0x00001000){$arrUAC += "ADS_UF_WORKSTATION_TRUST_ACCOUNT"}
		if ($uac -band 0x00002000){$arrUAC += "ADS_UF_SERVER_TRUST_ACCOUNT"}
		if ($uac -band 0x00010000){$arrUAC += "ADS_UF_DONT_EXPIRE_PASSWORD"}
		if ($uac -band 0x00020000){$arrUAC += "ADS_UF_MNS_LOGON_ACCOUNT"}
		if ($uac -band 0x00040000){$arrUAC += "ADS_UF_SMARTCARD_REQUIRED"}
		if ($uac -band 0x00080000){$arrUAC += "ADS_UF_TRUSTED_FOR_DELEGATION"}
		if ($uac -band 0x00100000){$arrUAC += "ADS_UF_NOT_DELEGATED"}
		if ($uac -band 0x00200000){$arrUAC += "ADS_UF_USE_DES_KEY_ONLY"}
		if ($uac -band 0x00400000){$arrUAC += "ADS_UF_DONT_REQUIRE_PREAUTH"}
		if ($uac -band 0x00800000){$arrUAC += "ADS_UF_PASSWORD_REQUIRED"}
		if ($uac -band 0x01000000){$arrUAC += "ADS_UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION"}
		$Object.UserAccountControl = $arrUAC
		
		return $Object
	}
}